package networks.protocol;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import networks.event.Delegate;

enum TransportState {

    NONE((byte) 0), // none
    READ_HEAD((byte) 1), // on read head
    READ_BODY((byte) 2), // on read body
    CLOSED((byte) 3); // connection closed, will ignore all the message and wait for clean up

    private final byte _value;

    private TransportState(byte value) {
        _value = value;
    }

    public byte ToByte() {
        return _value;
    }

    public static TransportState ToEnum(byte value) {
        for (TransportState e : TransportState.values()) {
            if (e.ToByte() == value)
                return e;
        }
        return TransportState.NONE;
    }
}

class StateObject {
    public final int BufferSize = 1024;
    public byte[] buffer = new byte[BufferSize];
}

public class Transporter {

    public static final int HEADER_LENGTH = 5;

    private Socket _socket;
    private Delegate.ActionOne<Package> _messageProcesser;

    // Used for get message
    private StateObject _stateObject = new StateObject();
    private TransportState _transportState;
    private boolean _onSending = false;
    private boolean _onReceiving = false;

    private byte[] _headBuffer = new byte[HEADER_LENGTH];
    private byte[] _msgBuffer;
    private int _bufferOffset = 0;
    private int _msgLength = 0;

    private InputStream _input;
    private Thread _receiveTread;

    private OutputStream _output;
    private Thread _sendTread;

    public Delegate.Action onDisconnect = null;

    public Transporter(Socket socket, Delegate.ActionOne<Package> processer) {
        _socket = socket;
        _messageProcesser = processer;
        _transportState = TransportState.READ_HEAD;

        try {
            _input = _socket.getInputStream();
            _receiveTread = new Thread() {
                public void run() {
                    receive();
                };
            };
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            _output = _socket.getOutputStream();
            _sendTread = new Thread() {
                public void run() {
                };
            };
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void Start() {
        // receive();
        _receiveTread.start();
    }

    public void Close() {
        _transportState = TransportState.CLOSED;
        try {
            if (_onReceiving) {
                _socket.shutdownInput();
            }
            if (_onSending) {
                _socket.shutdownOutput();
            }
            _socket.close();
        } catch (Exception e) {
            System.out.println(e.toString());
        }
    }

    public void Send(byte[] buffer) {
        try {
            OutputStream output = _socket.getOutputStream();
            _onSending = true;
            sendCallback(output, buffer);
        } catch (IOException e) {
            e.printStackTrace();
            _onSending = false;
        }
    }

    private void sendCallback(OutputStream output, byte[] buffer) throws IOException {
        if (_transportState == TransportState.CLOSED) {
            return;
        }

        // BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output,
        // StandardCharsets.UTF_8));
        BufferedOutputStream writer = new BufferedOutputStream(output);
        writer.write(buffer);
        writer.flush();
        // writer.close(); // 会关闭socket
        // _socket.shutdownOutput(); // 会断开连接
        _onSending = false;
    }

    private void receive() {
        if (_transportState == TransportState.CLOSED) {
            return;
        }

        // BufferedReader reader = new BufferedReader(new InputStreamReader(_input,
        // StandardCharsets.UTF_8));
        try {
            while (true) {
                int length = _input.read(_stateObject.buffer);
                // System.out.println("receive: " + length);
                _onReceiving = false;
                if (length > 0) {
                    System.out.println("receive: " + length);
                    processBytes(_stateObject.buffer, 0, length);
                    // Receive next message
                    if (_transportState != TransportState.CLOSED) {
                        receive();
                    }
                } else {
                    if (onDisconnect != null) {
                        // onDisconnect.Invoke();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void processBytes(byte[] bytes, int offset, int limit) {
        if (_transportState == TransportState.READ_HEAD) {
            readHead(bytes, offset, limit);
        } else if (_transportState == TransportState.READ_BODY) {
            readBody(bytes, offset, limit);
        }
    }

    /**
     * Head
     * 
     * @param bytes
     * @param offset
     * @param limit
     * @return
     */
    private boolean readHead(byte[] bytes, int offset, int limit) {
        int length = limit - offset;
        int headNum = HEADER_LENGTH - _bufferOffset;

        if (length >= headNum) {
            // Write head buffers
            writeBytes(bytes, 0, HEADER_LENGTH, _headBuffer);
            // Get package length
            _msgLength = ((_headBuffer[0] << 24) | (_headBuffer[1] << 16) | (_headBuffer[2] << 8) | _headBuffer[3]) - 1;

            // Init message buffer
            _msgBuffer = new byte[_msgLength];
            offset += headNum;
            _bufferOffset = 0;
            _transportState = TransportState.READ_BODY;

            if (offset <= limit) {
                processBytes(bytes, offset, limit);
            }
            return true;
        } else {
            writeBytes(bytes, offset, length, _bufferOffset, _headBuffer);
            _bufferOffset += length;
            return false;
        }
    }

    /**
     * Body
     * 
     * @param bytes
     * @param offset
     * @param limit
     */
    private void readBody(byte[] bytes, int offset, int limit) {
        if ((offset + _msgLength) <= limit) {
            // Get package type
            PackageType packageType = PackageType.ToEnum(bytes[HEADER_LENGTH - 1]);
            // Write msg buffers
            writeBytes(bytes, offset, _msgLength, _bufferOffset, _msgBuffer);
            offset += _msgLength;

            // Invoke the protocol api to handle the message
            Package pkg = new Package(packageType, _msgBuffer);
            _messageProcesser.Invoke(pkg);
            _bufferOffset = 0;
            _msgLength = 0;

            if (_transportState != TransportState.CLOSED) {
                _transportState = TransportState.READ_HEAD;
            }
            if (offset < limit) {
                processBytes(bytes, offset, limit);
            }
        } else {
            writeBytes(bytes, offset, limit - offset, _bufferOffset, _msgBuffer);
            _bufferOffset += limit - offset;
            _transportState = TransportState.READ_BODY;
        }
    }

    private void writeBytes(byte[] source, int start, int length, byte[] target) {
        writeBytes(source, start, length, 0, target);
    }

    private void writeBytes(byte[] source, int start, int length, int offset, byte[] target) {
        for (int i = 0; i < length; i++) {
            target[offset + i] = source[start + i];
        }
    }
}
